"use client"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { redirect, useRouter } from "next/navigation"; import Link from "next/link"; import AuthenticatedLayout from "@/components/AuthenticatedLayout"; import { AppointmentStatusBadge } from "@/components/appointments/AppointmentStatusBadge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { ApproveAppointmentModal } from "@/components/appointments/ApproveAppointmentModal"; import { Calendar, Clock, User, FileText, Video, CheckCircle2, XCircle, ArrowLeft, Loader2, AlertCircle, } from "lucide-react"; import { format } from "date-fns"; import { es } from "date-fns/locale"; import { toast } from "sonner"; import type { Appointment } from "@/types/appointments"; import { canJoinMeeting, getAppointmentTimeStatus } from "@/utils/appointments"; interface PageProps { params: Promise<{ id: string }>; } export default function AppointmentDetailPage({ params }: PageProps) { const router = useRouter(); const { data: session, status } = useSession(); const [appointment, setAppointment] = useState(null); const [loading, setLoading] = useState(true); const [approveDialog, setApproveDialog] = useState(false); const [rejectDialog, setRejectDialog] = useState(false); const [motivoRechazo, setMotivoRechazo] = useState(""); const [actionLoading, setActionLoading] = useState(false); const [appointmentId, setAppointmentId] = useState(""); useEffect(() => { const loadParams = async () => { const resolvedParams = await params; setAppointmentId(resolvedParams.id); }; loadParams(); }, [params]); useEffect(() => { if (!appointmentId) return; const fetchAppointment = async () => { try { const response = await fetch(`/api/appointments/${appointmentId}`); if (!response.ok) { throw new Error("No se pudo cargar la cita"); } const data: Appointment = await response.json(); setAppointment(data); } catch (error) { toast.error("Error al cargar la cita"); console.error(error); } finally { setLoading(false); } }; fetchAppointment(); }, [appointmentId]); if (status === "loading" || loading) { return (
); } if (!session) { redirect("/auth/login"); } if (!appointment) { return (

Cita no encontrada

La cita que buscas no existe o no tienes permisos para verla.

); } const userRole = session.user.role as "PATIENT" | "DOCTOR" | "ADMIN"; const isPatient = userRole === "PATIENT"; const isDoctor = userRole === "DOCTOR"; const otherUser = isPatient ? appointment.medico : appointment.paciente; const hasFecha = appointment.fechaSolicitada !== null; const fecha = hasFecha ? new Date(appointment.fechaSolicitada!) : null; const handleApprove = async (fechaSolicitada: Date, notas?: string) => { setActionLoading(true); try { const response = await fetch(`/api/appointments/${appointment.id}/approve`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ fechaSolicitada: fechaSolicitada.toISOString(), notas, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Error al aprobar la cita"); } const updated: Appointment = await response.json(); setAppointment(updated); setApproveDialog(false); toast.success("Cita aprobada exitosamente"); } catch (error) { toast.error(error instanceof Error ? error.message : "Error al aprobar la cita"); console.error(error); } finally { setActionLoading(false); } }; const handleRejectConfirm = async () => { if (!motivoRechazo.trim()) return; setActionLoading(true); try { const response = await fetch(`/api/appointments/${appointment.id}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ motivoRechazo }), }); if (!response.ok) throw new Error("Error al rechazar la cita"); const updated: Appointment = await response.json(); setAppointment(updated); setRejectDialog(false); setMotivoRechazo(""); toast.success("Cita rechazada"); } catch (error) { toast.error("Error al rechazar la cita"); console.error(error); } finally { setActionLoading(false); } }; const handleCancel = async () => { setActionLoading(true); try { const response = await fetch(`/api/appointments/${appointment.id}`, { method: "DELETE", }); if (!response.ok) throw new Error("Error al cancelar la cita"); toast.success("Cita cancelada exitosamente"); router.push("/appointments"); } catch (error) { toast.error("Error al cancelar la cita"); console.error(error); setActionLoading(false); } }; const handleStartMeeting = async () => { setActionLoading(true); try { const response = await fetch(`/api/appointments/${appointment.id}/start-meeting`, { method: "POST", }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || error.error || "No se puede iniciar la videollamada"); } const data = await response.json(); // Redirigir a la sala de Jitsi router.push(`/appointments/${appointment.id}/meet`); } catch (error) { toast.error(error instanceof Error ? error.message : "Error al iniciar videollamada"); console.error(error); } finally { setActionLoading(false); } }; const handleComplete = async () => { setActionLoading(true); try { const response = await fetch(`/api/appointments/${appointment.id}/complete`, { method: "POST", }); if (!response.ok) throw new Error("Error al completar la cita"); const updated: Appointment = await response.json(); setAppointment(updated); toast.success("Cita marcada como completada"); } catch (error) { toast.error("Error al completar la cita"); console.error(error); } finally { setActionLoading(false); } }; return (
{/* Back Button */} {/* Header Card */}
{otherUser && ( {otherUser.name[0]}{otherUser.lastname[0]} )}
{otherUser ? `${otherUser.name} ${otherUser.lastname}` : isDoctor ? "Sin asignar" : "Médico por asignar"} {isPatient ? "Médico asignado" : "Paciente"}
{/* Details Card */} Detalles de la Cita {hasFecha && fecha ? (

Fecha

{format(fecha, "PPP", { locale: es })}

Hora

{format(fecha, "p", { locale: es })}

) : (

Fecha y hora

{appointment.estado === "PENDIENTE" ? "Pendiente de asignación por el médico" : "No asignada"}

)}

Motivo de consulta

{appointment.motivoConsulta}

{appointment.motivoRechazo && ( <>

Motivo de rechazo

{appointment.motivoRechazo}

)} {/* Solo mostrar sala si NO está completada */} {appointment.roomName && appointment.estado !== "COMPLETADA" && ( <>
)} {appointment.notasGuardadas && appointment.notasConsulta && ( <>

Notas de la Consulta

{appointment.notasGuardadasAt && (

Guardadas el {format(new Date(appointment.notasGuardadasAt), "d 'de' MMMM 'a las' HH:mm", { locale: es })}

)}
{appointment.notasConsulta}
)}
{/* Actions Card */} Acciones
{isDoctor && appointment.estado === "PENDIENTE" && ( <> )} {isDoctor && appointment.estado === "APROBADA" && ( <> {canJoinMeeting(appointment.fechaSolicitada).canJoin ? ( ) : ( )} )} {isPatient && appointment.estado === "PENDIENTE" && ( )} {isPatient && appointment.estado === "APROBADA" && ( <> {canJoinMeeting(appointment.fechaSolicitada).canJoin ? ( ) : ( )} )} {/* Acciones para citas completadas */} {appointment.estado === "COMPLETADA" && ( <> {appointment.notasGuardadas && appointment.notasConsulta ? (

Consulta Finalizada

Las notas de la consulta están disponibles arriba

) : (

Consulta Finalizada

Esta cita ha sido completada

)} )} {/* Botón genérico de unirse (solo para APROBADA, no COMPLETADA) */} {appointment.estado === "APROBADA" && canJoinMeeting(appointment.fechaSolicitada).canJoin && ( )}
{/* Reject Dialog */} Rechazar Cita Por favor proporciona un motivo para rechazar esta cita. El paciente recibirá esta información.